<?php
/**
 * Note: 网页授权获得auth_code, 然后获取accessToken, 查询授权账户(纵横组织)ID, 查询纵横组织下广告主ID, 后续根据广告主ID查询数据
 */
namespace App\Services\Promo;

use Carbon\Carbon;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class DouyinService
{
    protected const API_ACCESS_TOKEN = 'https://ad.oceanengine.com/open_api/oauth2/access_token/';
    protected const API_REFRESH_TOKEN = 'https://ad.oceanengine.com/open_api/oauth2/refresh_token/';
    protected const API_ACCOUNT_INFO = 'https://ad.oceanengine.com/open_api/oauth2/advertiser/get/';
    protected const API_ACCOUNT_REPORT = 'https://ad.oceanengine.com/open_api/2/report/advertiser/get/';
    protected const API_CAMPAIGN_REPORT = 'https://ad.oceanengine.com/open_api/2/report/campaign/get/';
    protected const API_CUSTOM_REPORT = 'https://api.oceanengine.com/open_api/v3.0/report/custom/get/';
    protected const API_CUSTOM_REPORT_CONFIG = 'https://api.oceanengine.com/open_api/v3.0/report/custom/config/get/';

    // 纵横组织下资产账户列表
    protected const API_MAJORDOMO_ADVERTISER = 'https://ad.oceanengine.com/open_api/2/majordomo/advertiser/select/';
    protected const API_CUSTOMER_CENTER_ADVERTISER = 'https://ad.oceanengine.com/open_api/2/customer_center/advertiser/list/';

    protected static array $configs = [];

    public const KEY_ACCESS_TOKEN = 'access_token';
    public const KEY_REFRESH_TOKEN = 'refresh_token';
    public const KEY_ACCOUNT_IDS = 'account_ids';
    public const KEY_ADVERTISERS = 'advertisers';
    public const KEY_ADVERTISER_IDS = 'advertiser_ids';

    /**
     * @param null $key
     * @return array|\Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed|null
     */
    protected function getConfig($key = null)
    {
        if (!self::$configs) self::$configs = config('promo.douyin');

        return $key ? (self::$configs[$key] ?? null) : self::$configs;
    }

    public function get(string $key, $default = null)
    {
        // 优先获取新缓存, 如果未获取到, 就尝试获取旧缓存
        $key = __CLASS__ . $key;
        $data = Cache::get($key, $default);
        if (isset($data)) return $data;

        // 兼容旧缓存
        return Cache::get(str_replace('\Promo', '', $key), $default);
    }

    public function set(string $key, $val, $ttl = null)
    {
        $key = __CLASS__.$key;
        Cache::put($key, $val, $ttl);
    }

    /**
     * 发送get请求
     * @param $url
     * @param $data
     * @param array $header
     * @param bool $auth
     * @return false|\GuzzleHttp\Promise\PromiseInterface|Response
     * @throws \Exception
     */
    protected function requestGet($url, $data, array $header = [], bool $auth = false)
    {
        if ($auth) {
            $header['Access-Token'] = $this->getAccessToken();
            if (!$header['Access-Token']) return false;
        }
        $header['X-Debug-Mode'] = 1;
        // 发生错误时 100ms 后重试 1 次, 每次超时3s (设置timeout之后, 如果超时会直接抛出异常, 不到 $response->throw), 这里放入try
        try {
            info(__METHOD__, [$url, $data]);
            $response = Http::retry(2, 100)->timeout(3)->withHeaders($header)->asForm()->get($url, $data);
            $response->throw();
            info(__METHOD__, [$response]);
        } catch (\Exception $e) {
            report($e);
            throw new \Exception("抖音正在维护中，请联系客服");
        }
        return $response;
    }

    /**
     * 发送post请求
     * @param string $url
     * @param array $data
     * @param array $header
     * @param bool $auth
     * @return false|\GuzzleHttp\Promise\PromiseInterface|Response
     * @throws \Exception
     */
    protected function requestPost(string $url, array $data, array $header = [], bool $auth = false)
    {
        if ($auth) {
            $header['Access-Token'] = $this->getAccessToken();
            if (!$header['Access-Token']) return false;
        }

        // 发生错误时 100ms 后重试 1 次, 每次超时3s (设置timeout之后, 如果超时会直接抛出异常, 不到 $response->throw), 这里放入try
        try {
            info(__METHOD__, [$url, $data]);
            $response = Http::retry(2, 100)->timeout(3)->withHeaders($header)->asJson()->post($url, $data);
            $response->throw();
            // info(__METHOD__, [$response]);
            if ($response['code'] > 0) throw new \Exception($response['message']);
        } catch (\Exception $e) {
            report($e);
            throw new \Exception($e->getMessage());
        }
        return $response;
    }

    /**
     * 获取身份鉴权用的access_token
     * @param string $authCode 在快手开发者后台使用回调接口获得
     * @return string
     * @throws \Exception
     */
    public function getAccessToken(string $authCode = ''): string
    {
        // 重新授权
        if ($authCode) {
            $param = [
                'app_id' => $this->getConfig('app_id'),
                'secret' => $this->getConfig('secret'),
                'grant_type' => 'auth_code',
                'auth_code' => $authCode,
            ];
            $response = $this->requestPost(self::API_ACCESS_TOKEN, $param);
            // 缓存
            $this->cacheResponse($response);

            return $response['data']['access_token'];
        }

        // 使用缓存, 没有缓存则使用refresh_token进行刷新
        $accessToken = $this->get(self::KEY_ACCESS_TOKEN);
        if ($accessToken) return $accessToken;

        return $this->refreshToken();
    }

    /**
     * @return string
     * @throws \Exception
     */
    public function refreshToken(): string
    {
        if (!$this->get(self::KEY_REFRESH_TOKEN)) {
            throw new \Exception('刷新token失败, 本地无token缓存, 请先获取授权');
        }
        $param = [
            'app_id' => $this->getConfig('app_id'),
            'secret' => $this->getConfig('secret'),
            'grant_type' => 'refresh_token',
            'refresh_token' => $this->get(self::KEY_REFRESH_TOKEN),
        ];
        $response = $this->requestPost(self::API_REFRESH_TOKEN, $param);

        // 缓存
        $this->cacheResponse($response);

        return $response['data']['access_token'];
    }

    /**
     * 由于此方法只会在刷新accessToken后运行, 刷新时间是一天, 若在此期间新建了广告主ID, 系统会有一天的延迟, 此时可以重新授权, 获得最新的数据
     * @param Response $response
     * @throws \Exception
     */
    private function cacheResponse(Response $response)
    {
        $this->set(self::KEY_ACCESS_TOKEN, $response['data']['access_token'], $response['data']['expires_in'] - 30);
        $this->set(self::KEY_REFRESH_TOKEN, $response['data']['refresh_token'], $response['data']['refresh_token_expires_in'] - 30);
        // 获取并存储纵横组织账户ID
        $this->refreshAccountIds();
        // 获取并纵横组织账户下广告主ID
        $this->refreshAdvertiserIds();
    }

    /**
     * 获取账户信息并存储纵横账号ID
     * @throws \Exception
     */
    public function refreshAccountIds()
    {
        $response = $this->requestGet(self::API_ACCOUNT_INFO, [
            'access_token' => $this->get(self::KEY_ACCESS_TOKEN),
            'app_id' => $this->getConfig('app_id'),
            'secret' => $this->getConfig('secret'),
        ]);
        if (!$response) throw new \Exception('获取账户信息失败');
        if (empty($response['data']['list'])) throw new \Exception('已授权账户为空');
        $this->set(self::KEY_ACCOUNT_IDS, array_column($response['data']['list'], 'advertiser_id'));
    }

    /**
     * 获取纵横组织下的广告主ID
     * @throws \Exception
     */
    // public function refreshAdvertiserIds()
    // {
    //     $majordomoIds = $this->get(self::KEY_ACCOUNT_IDS);
    //     $majordomoAdvertisers = [];
    //     foreach ($majordomoIds as $majordomoId) {
    //         $param = [
    //             'advertiser_id' => $majordomoId,
    //         ];
    //         $response = $this->requestGet(self::API_MAJORDOMO_ADVERTISER, $param, [], true);
    //         if (!$response || empty($response['data']['list'])) continue;
    //         array_push($majordomoAdvertisers, ...array_column($response['data']['list'], 'advertiser_id'));
    //     }
    //     $this->set(self::KEY_ADVERTISER_IDS, $majordomoAdvertisers);
    // }

    /**
     * 获取工作台下账户列表
     * @throws \Exception
     */
    public function refreshAdvertiserIds()
    {
        $majordomoIds = $this->get(self::KEY_ACCOUNT_IDS);
        $majordomoAdvertisers = [];
        foreach ($majordomoIds as $majordomoId) {
            $param = [
                'cc_account_id' => $majordomoId,
                'page_size' => 100,
            ];
            $response = $this->requestGet(self::API_CUSTOMER_CENTER_ADVERTISER, $param, [], true);
            if (!$response || empty($response['data']['list'])) continue;
            array_push($majordomoAdvertisers, ...$response['data']['list']);
        }
        $ads = [];
        foreach ($majordomoAdvertisers as $v) {
            $ads[] = [
                'id' => $v['advertiser_id'],
                'name' => $v['advertiser_name'],
            ];
        }
        $this->set(self::KEY_ADVERTISERS, $ads);
        $this->set(self::KEY_ADVERTISER_IDS, $majordomoAdvertisers);
    }

    /**
     * 广告主数据
     * @param int $adid
     * @param array $timeRange
     * @param int $page
     * @param int $pageSize
     * @return array
     * @throws \Exception
     */
    public function accountReport(int $adid, array $timeRange = [], int $page = 1, int $pageSize = 100): array
    {
        $timeRange = array_map(function ($v) {
            if ($v instanceof Carbon) return $v->format('Y-m-d');
            return $v;
        }, $timeRange);

        $param = [
            'advertiser_id' => $adid,
            'start_date' => $timeRange[0],
            'end_date' => $timeRange[1],
            'page' => $page,
            'page_size' => $pageSize,
        ];
        $response = $this->requestPost(self::API_ACCOUNT_REPORT, $param, [], true);
        if (!$response) return [];
        return $response['data']['list'] ?? [];
        // 花费 data.details.charge
    }

    /**
     * 广告计划数据
     * @param int $adid
     * @param array $timeRange
     * @param int $page
     * @param int $pageSize
     * @return array
     * @throws \Exception
     */
    public function campaignReport(int $adid, array $timeRange = [], int $page = 1, int $pageSize = 100): array
    {
        $timeRange = array_map(function ($v) {
            if ($v instanceof Carbon) return $v->format('Y-m-d');
            return $v;
        }, $timeRange);

        $param = [
            'advertiser_id' => $adid,
            'group_by' => json_encode(['STAT_GROUP_BY_FIELD_ID']),
            'start_date' => $timeRange[0],
            'end_date' => $timeRange[1],
            'page' => $page,
            'page_size' => $pageSize,
        ];
        $response = $this->requestGet(self::API_CAMPAIGN_REPORT, $param, [], true);
        if (!$response) return [];
        return $response['data']['list'] ?? [];
        // 花费 data.details.charge
        // 广告计划ID data.details.campaign_id
        // 广告计划名称 data.details.campaign_name
    }

    /**
     * 广告计划数据
     * @param int $adid
     * @param array $timeRange
     * @param int $page
     * @param int $pageSize
     * @return array
     * @throws \Exception
     */
    public function projectReport(int $adid, array $timeRange = [], int $page = 1, int $pageSize = 100): array
    {
        $timeRange = array_map(function ($v) {
            if ($v instanceof Carbon) return $v->format('Y-m-d');
            return $v;
        }, $timeRange);

        // [数据主题] - [维度] - [指标列表]
        // 基础数据 - 项目名称 - 推广费
        $param = [
            'advertiser_id' => $adid,
            'data_topic' => 'BASIC_DATA',
            'dimensions' => json_encode(['cdp_project_id', 'cdp_project_name']),
            'metrics' => json_encode(['stat_cost']),
            'filters' => json_encode([]),
            // 'filters' => [
            //     'field' => '',
            //     'type' => 3,
            //     'operator' => 4,
            //     'values' => 0,
            // ],
            'start_time' => $timeRange[0],
            'end_time' => $timeRange[1],
            'order_by' => json_encode([]),
            'page' => $page,
            'page_size' => $pageSize,
        ];
        $response = $this->requestGet(self::API_CUSTOM_REPORT, $param, [], true);
        if (!$response) return [];
        return $response['data']['rows'] ?? [];
        // 项目名称 data.dimensions.cdp_project_name
        // 花费 data.metrics.stat_cost
    }

    /**
     * 获取自定义报表可用指标和维度
     * @param int $adid
     * @param array $dataTopics
     * @return array
     * @throws \Exception
     */
    public function getReportConfig(int $adid, array $dataTopics = ['BASIC_DATA']): array
    {
        $param = [
            'advertiser_id' => $adid,
            'data_topics' => json_encode($dataTopics),
        ];
        $response = $this->requestGet(self::API_CUSTOM_REPORT_CONFIG, $param, [], true);
        if (!$response) return [];
        return $response['data']['list'] ?? [];
        // 维度列表 data.dimensions
        // 指标列表 data.metrics
    }
}
